import _ from 'lodash';
import { Observable } from 'rx';

import { unDasherize, nameify } from '../../../utils/slugs';
import {
  addNameIdMap as _addNameIdToMap,
  checkMapData,
  getFirstChallenge as _getFirstChallenge
} from '../../common/utils/map.js';

const isDev = process.env.FREECODECAMP_NODE_ENV !== 'production';
const isBeta = !!process.env.BETA;
const challengesRegex = /^(bonfire|waypoint|zipline|basejump|checkpoint)/i;
const addNameIdMap = _.once(_addNameIdToMap);
const getFirstChallenge = _.once(_getFirstChallenge);
/*
 * interface ChallengeMap {
 *   result: {
 *     superBlocks: [ ...superBlockDashedName: String ]
 *    },
 *   entities: {
 *     superBlock: {
 *       [ ...superBlockDashedName ]: SuperBlock
 *     },
 *     block: {
 *       [ ...blockDashedNameg ]: Block,
 *     challenge: {
 *       [ ...challengeDashedNameg ]: Challenge
 *     }
 *   }
 * }
 */
export function _cachedMap({ Block, Challenge }) {
  const challenges = Challenge.find$({
    order: ['order ASC', 'suborder ASC'],
    where: { isPrivate: false }
  });
  const challengeMap = challenges.map(challenges =>
    challenges
      .map(challenge => challenge.toJSON())
      .reduce((hash, challenge) => {
        hash[challenge.dashedName] = challenge;
        return hash;
      }, {})
  );
  const blocks = Block.find$({
    order: ['superOrder ASC', 'order ASC'],
    where: { isPrivate: false }
  });
  const blockMap = Observable.combineLatest(
    blocks.map(blocks =>
      blocks
        .map(block => block.toJSON())
        .reduce((hash, block) => {
          hash[block.dashedName] = block;
          return hash;
        }, {})
    ),
    challenges
  ).map(([blocksMap, challenges]) => {
    return challenges.reduce((blocksMap, challenge) => {
      if (blocksMap[challenge.block].challenges) {
        blocksMap[challenge.block].challenges.push(challenge.dashedName);
      } else {
        blocksMap[challenge.block] = {
          ...blocksMap[challenge.block],
          challenges: [challenge.dashedName]
        };
      }
      return blocksMap;
    }, blocksMap);
  });
  const superBlockMap = blocks.map(blocks =>
    blocks.reduce((map, block) => {
      if (map[block.superBlock] && map[block.superBlock].blocks) {
        map[block.superBlock].blocks.push(block.dashedName);
      } else {
        map[block.superBlock] = {
          title: _.startCase(block.superBlock),
          order: block.superOrder,
          name: nameify(_.startCase(block.superBlock)),
          dashedName: block.superBlock,
          blocks: [block.dashedName],
          message: block.superBlockMessage
        };
      }
      return map;
    }, {})
  );
  const superBlocks = superBlockMap.map(superBlockMap => {
    return Object.keys(superBlockMap)
      .map(key => superBlockMap[key])
      .map(({ dashedName }) => dashedName);
  });
  return Observable.combineLatest(
    superBlockMap,
    blockMap,
    challengeMap,
    superBlocks,
    (superBlock, block, challenge, superBlocks) => ({
      entities: {
        superBlock,
        block,
        challenge
      },
      result: {
        superBlocks
      }
    })
  )
    .do(checkMapData)
    .shareReplay();
}

export const cachedMap = _.once(_cachedMap);

// type ObjectId: String;
// getChallengeById(
//   map: Observable[map],
//   id: ObjectId
// ) => Observable[Challenge] | Void;
export function getChallengeById(map, id) {
  return Observable.if(
    () => !id,
    map.map(getFirstChallenge),
    map.map(addNameIdMap).map(map => {
      const {
        entities: { challenge: challengeMap, challengeIdToName }
      } = map;
      let finalChallenge;
      const dashedName = challengeIdToName[id];
      finalChallenge = challengeMap[dashedName];
      if (!finalChallenge) {
        finalChallenge = getFirstChallenge(map);
      }
      return finalChallenge;
    })
  );
}

export function getChallengeInfo(map) {
  return map
    .map(addNameIdMap)
    .map(({ entities: { challenge: challengeMap, challengeIdToName } }) => ({
      challengeMap,
      challengeIdToName
    }));
}

// if challenge is not isComingSoon or isBeta => load
// if challenge is ComingSoon we are in beta||dev => load
// if challenge is beta and we are in beta||dev => load
// else hide
function loadComingSoonOrBetaChallenge({
  isComingSoon,
  isBeta: challengeIsBeta
}) {
  return !(isComingSoon || challengeIsBeta) || isDev || isBeta;
}

// this is a hard search
// falls back to soft search
export function getChallenge(challengeDashedName, blockDashedName, map) {
  return map.flatMap(({ entities, result: { superBlocks } }) => {
    const superBlock = entities.superBlock;
    const block = entities.block[blockDashedName];
    const challenge = entities.challenge[challengeDashedName];
    return Observable.if(
      () =>
        !blockDashedName ||
        !block ||
        !challenge ||
        !loadComingSoonOrBetaChallenge(challenge),
      getChallengeByDashedName(challengeDashedName, map),
      Observable.just({ block, challenge })
    ).map(({ challenge, block }) => ({
      redirect:
        challenge.block !== blockDashedName
          ? `/challenges/${block.dashedName}/${challenge.dashedName}`
          : false,
      entities: {
        superBlock,
        challenge: {
          [challenge.dashedName]: challenge
        }
      },
      result: {
        block: block.dashedName,
        challenge: challenge.dashedName,
        superBlocks
      }
    }));
  });
}

export function getBlockForChallenge(map, challenge) {
  return map.map(({ entities: { block } }) => block[challenge.block]);
}

export function getChallengeByDashedName(dashedName, map) {
  const challengeName = unDasherize(dashedName).replace(challengesRegex, '');
  const testChallengeName = new RegExp(challengeName, 'i');

  return map
    .map(({ entities }) => entities.challenge)
    .flatMap(challengeMap => {
      return Observable.from(Object.keys(challengeMap)).map(
        key => challengeMap[key]
      );
    })
    .filter(challenge => {
      return (
        loadComingSoonOrBetaChallenge(challenge) &&
        testChallengeName.test(challenge.name)
      );
    })
    .last({ defaultValue: null })
    .flatMap(challengeOrNull => {
      return Observable.if(
        () => !!challengeOrNull,
        Observable.just(challengeOrNull),
        map.map(getFirstChallenge)
      );
    })
    .flatMap(challenge => {
      return getBlockForChallenge(map, challenge).map(block => ({
        challenge,
        block
      }));
    });
}
